package de.koller.oceanFramework.gl;

import de.koller.oceanFramework.OceanException;
import de.koller.oceanFramework.gl.util.DisplayModeChooser;
import de.koller.oceanFramework.gl.util.TextureUtil;
import de.koller.oceanFramework.thread.IPipe;
import de.koller.oceanFramework.thread.Pipeline;
import de.koller.oceanFramework.tree.ISea3;
import java.awt.Canvas;
import java.io.PrintStream;
import java.nio.ByteBuffer;
import org.lwjgl.LWJGLException;
import org.lwjgl.Sys;
import org.lwjgl.input.Keyboard;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.PixelFormat;
import org.lwjgl.util.glu.GLU;

/**
 * 
 * @author Fabian Koller
 */
public class OceanGame implements IOceanGame {
	/**
	 * 
	 */
	public static final int MAX_UPDATE_TIME = (int)(1000f/20f);
	
	private long lastFrame;
	private int fps;
	private long lastFPS;
	private int currFPS;
	private boolean makeUpdate = true;
	private double unprocessedSeconds = 0;
	private long lastTimeDebug = 0;
	private long usedSpace = 0;
	private long maxSpace = 0;
	
	private boolean isRender = true;
	private boolean isUpdate = true;
	
	private final int width;
	private final int height;
	private final String title;
	
	private boolean running = true;
	private final boolean fullscreen;
	private boolean vSync = false;
	
	public static boolean enableClientChoose = false;
	private String ip = null;
	private boolean asClient = false;
	
	private float frameTimeAverage = 0;
	private float updateTimeAverage = 0;
	
	private final ISea3 root = new OceanRoot();

	/**
	 * 
	 */
	public boolean projection;
	
	private final DisplayMode displayMode;
	
	/**
	 * 
	 * @param title
	 * @return
	 */
	public static OceanGame create( String title ) {
		return create( title, -1, -1, -1, -1, false, false );
	}
	
	public static void checkForErrorGL( String message ) {
		int e = GL11.glGetError();
		if( e != GL11.GL_NO_ERROR ) {
			if( e == GL11.GL_INVALID_ENUM )
				throw new OceanException( "Invalid enum:" + message );		
			else if( e == GL11.GL_INVALID_VALUE )
				throw new OceanException( "Invalid value:" + message );	
			else if( e == GL11.GL_INVALID_OPERATION )
				throw new OceanException( "Invalid Operation:" + message );	
			else if( e == GL11.GL_STACK_OVERFLOW )
				throw new OceanException( "Stack overflow:" + message );	
			else if( e == GL11.GL_STACK_UNDERFLOW )
				throw new OceanException( "Stack underflow:" + message );	
			else if( e == GL11.GL_OUT_OF_MEMORY )
				throw new OceanException( "Out of memory:" + message );	
			else
				throw new OceanException( "OpenGLError 0x" + Integer.toHexString( e ) + ":" + message );	
		}
	}
	
	/**
	 * 
	 * @param title
	 * @param width
	 * @param height
	 * @param depth
	 * @param freq
	 * @param filter
	 * @param full
	 * @return
	 */
	public static OceanGame create( String title, int width, int height, int depth, int freq, boolean filter, boolean full ) {
		final DisplayModeChooser dmc = new DisplayModeChooser();
		if( width != -1 )
			dmc.setWished( width, height, depth, freq, filter, full );
		
		Pipeline pl = new Pipeline( new IPipe() {
			@Override public void updateSubStep() {
				synchronized( dmc ) {
					try {
						while( dmc.getChoosedMode() == null )
							dmc.wait();
					} catch( InterruptedException ex ) { 
						System.err.println( ex ); 
					}
				}
			}
			@Override public String getName() { return "DMC"; }
		} );
	
		pl.stepPipes();
		
		OceanGame game = new OceanGame( dmc.getChoosedMode(), title, true, dmc.isFullscreen(), dmc.isVsync() );
		game.setAsClient( dmc.isAsClient() );
		game.setIp( dmc.getIP() );
		
		return game;
	}
	
	/**
	 * 
	 * @param width
	 * @param height
	 * @param title
	 * @deprecated use {@link #create(java.lang.String, int, int, int, int, boolean, boolean)} instead.
	 */
	@Deprecated
	public OceanGame( int width, int height, String title ) {
		this( width, height, title, true, false );
	}
	
	/**
	 * 
	 * @param width
	 * @param height
	 * @param title
	 * @param projection
	 * @deprecated use {@link #create(java.lang.String, int, int, int, int, boolean, boolean)} instead.
	 */
	@Deprecated
	public OceanGame( int width, int height, String title, boolean projection ) {
		this( width, height, title, projection, false );
	}

	/**
	 * 
	 * @param width
	 * @param height
	 * @param title
	 * @param projection
	 * @param fullscreen
	 * @deprecated use {@link #create(java.lang.String, int, int, int, int, boolean, boolean)} instead.
	 */
	@Deprecated
	public OceanGame( int width, int height, String title, boolean projection, boolean fullscreen ) {
		this( new DisplayMode( width, height ), title, projection, fullscreen, false );
	}
	
	/**
	 * 
	 * @param mode
	 * @param title
	 * @param projection
	 * @param fullscreen
	 * @param vsync
	 * @deprecated use {@link #create(java.lang.String, int, int, int, int, boolean, boolean)} instead.
	 */
	@Deprecated
	public OceanGame( DisplayMode mode, String title, boolean projection, boolean fullscreen, boolean vsync ) {
		this.displayMode = mode;
		this.width = mode.getWidth();
		this.height = mode.getHeight();
		this.title = title;
		this.projection = projection;
		this.fullscreen = fullscreen;
		this.vSync = vsync;
		root.setGame( this );
	}
	
	public OceanGame( DisplayMode mode, String title, boolean fullscreen, boolean vsync ) {
		this( mode, title, false, fullscreen, vsync );
	}
	
	/**
	 * 
	 */
	public void start() {
		this.start( null );
	}
	
	/**
	 * 
	 * @param parent
	 * @deprecated not yet supported, use {@link #start()} instead.
	 */
	@Deprecated
	public void start( Canvas parent ) {
		try {
			if( parent == null ) {
//				GameContainer gc = new AppGameContainer( null );
//				gc.setIcons( new String[] {
//					"data/icons/icon132.png",
//					"data/icons/icon116.png",
//				} );
				
				Display.setIcon( new ByteBuffer[] {
					TextureUtil.load( "data/icons/icon132.png" ).generateByteBuffer(),
					TextureUtil.load( "data/icons/icon116.png" ).generateByteBuffer()
				} );
				
				Display.setDisplayMode( displayMode );
				Display.setFullscreen( fullscreen );
				if( fullscreen )
					Display.setVSyncEnabled( vSync );
				Display.setTitle( title );
			}
			if( parent != null ) {
				System.out.println( parent );
				Display.setParent( parent );
			} 
			Display.create( new PixelFormat( 32, 0, 24, 8, 0 ) );
		} catch( Exception e ) {
			e.printStackTrace();
			System.exit(0);
		}
		 
		initGL();
		getDelta();
		lastFPS = getTime();
 
		root.initalize();
		
//		while (!Display.isCloseRequested() && running) {
//			int delta = getDelta();
//			if( delta > MAX_UPDATE_TIME ) {
//				System.err.println( "Zu langsame FPS, läuft langsamer! " + delta + "ms -> " + MAX_UPDATE_TIME + "ms" );
//				delta = MAX_UPDATE_TIME;
//			}
//			
//			if( delta > 0 )
//				update(delta);
//			updateFPS(); // update FPS Counter
//			renderGL( delta );
//
//			Display.update();
////			Display.sync(60);
//		}

		limitedLoop();
		
		try {
			Display.setFullscreen( false );
		} catch( LWJGLException ex ) { System.err.println( ex ); }
		root.releasing();
		Display.destroy();
		
//		System.exit( 1 );
	}
	
	private void updateUL( int elapsedTime ) {
		// Unabhängige Zeitmessung
//		int delta = getDelta();
//		if( delta > MAX_UPDATE_TIME ) {
//			System.err.println( "Zu langsame FPS, läuft langsamer! " + delta + "ms -> " + MAX_UPDATE_TIME + "ms" );
//			delta = MAX_UPDATE_TIME;
//		}
			
		if( elapsedTime > 0 )
			update(elapsedTime);
	}
	
	private void renderUL( int elapsedTime ) {
		updateFPS(); // update FPS Counter
		renderGL( elapsedTime );

		Display.update();
	}
	
	private void limitedLoop() {
		int frames = 0;
		long lastTime = getTime() * 1_000_000L;
		double secondsPerTick = 1d / 60d;
		int tickCount = 0;

		long timePerLoop = 0;
		long updateTimePerLoop = 0;
		long maxTime = 0;
		long maxUpdateTime = 0;
		
		int slowFrames = 0;
		
		while( running && !Display.isCloseRequested() ) {
			long now = getTime() * 1_000_000L;
			long passedTime = now - lastTime;
					
			lastTime = now;
			
			if( passedTime < 0 ) passedTime = 0;
			if( passedTime > 100_000_000 ) passedTime = 100_000_000;
			unprocessedSeconds += passedTime / 1000_000_000d;
			
			if( unprocessedSeconds > secondsPerTick ) {
				long independetTime = getTime() * 1_000_000L;
				
				if( isUpdate )
					updateUL( (int)(secondsPerTick * 1000d) );
				long udt = getTime() * 1_000_000L - independetTime;
				
				if( isRender )
					renderUL( (int)(secondsPerTick * 1000d) );
				
				unprocessedSeconds -= secondsPerTick;

				frames++;
				
				long dt = getTime() * 1_000_000L - independetTime;

				if( dt > secondsPerTick*1_000_000_000L )
					slowFrames++;
				else
					slowFrames--;
				
				if( slowFrames > 5 ) {
					System.err.println( "Warning: Slow Game!" );
					slowFrames = 0;
				}
				
				timePerLoop += dt;
				updateTimePerLoop += udt;
				maxTime = Math.max( dt, maxTime );
				maxUpdateTime = Math.max( udt, maxUpdateTime );
				lastTimeDebug = dt;

				tickCount++;
				if( tickCount % 100 == 0 ) {
					double tpl = ( (double)timePerLoop / 100d / 1_000_000d);
					double mat = ( (double)maxTime / 1_000_000d);
					double per = Math.round( tpl / (secondsPerTick*1000d) * 10d ) / 10d * 100d;
					
					double utpl = ( (double)updateTimePerLoop / 100d / 1_000_000d );
					double umat = ( (double)maxUpdateTime / 1_000_000d );
					
					PrintStream ps;
					if( per > 99 )
						ps = System.err;
					else
						ps = System.out;
	

					//@todo notify the user if the game runs to slow!
					//OceanRoot r = (OceanRoot)this.getRoot();
					frameTimeAverage = (float)( Math.round( tpl*100d )/100d );
					updateTimeAverage = (float)( Math.round( utpl*100d )/100d );
					
					ps.println( frames + " frames: " + frameTimeAverage + " ms/frame, "
							+ "max: " + (Math.round( mat*100d )/100d) +" ms)"
							+ "(update: " + updateTimeAverage + " ms/frame, "
							+ "max: " + (Math.round( umat*100d )/100d) +" ms)"
							+ " [" + currFPS + " (" + usedSpace + " / " + maxSpace + " MB) (c: " + root.countChilds() + ")]" );
					
					timePerLoop = 0;
					maxTime = 0;
					
					updateTimePerLoop = 0;
					maxUpdateTime = 0;
					
					lastTime += 1000;
					frames = 0;
				}
				
//				Display.sync( 60 );
			}

		}
	}
	
	/**
	 * 
	 * @param delta
	 */
	public final void update( int delta ) {
		if( Keyboard.isKeyDown( Keyboard.KEY_F12 ) )
			running = false;
		
		
		if( makeUpdate ) {
//			makeUpdate = false;
			root.updating( delta );
		}
	}
 
	
	/**
	 * 
	 * @return
	 */
	public int getDelta() {
		long time = getTime();
		int delta = (int) (time - lastFrame);
		lastFrame = time;

		return delta;
	}
 
	/**
	 * 
	 * @return
	 */
	public long getTime() {
		return (Sys.getTime() * 1000) / Sys.getTimerResolution();
	}
 

	/**
	 * 
	 */
	public void updateFPS() {
		if( getTime() - lastFPS > 1000 ) {
//			Display.setTitle( "FPS: " + fps );
			currFPS = fps;
			fps = 0;
			lastFPS += 1000;
			
			long tm = Runtime.getRuntime().totalMemory() / 1024 / 1024;
			long fm = Runtime.getRuntime().freeMemory() / 1024 / 1024;
			maxSpace = tm;
			usedSpace = (tm - fm);
			
//			System.out.println( currFPS );
		}
		fps++;
	}
 
	/**
	 * 
	 */
	public void initGL() {
		GL11.glShadeModel( GL11.GL_SMOOTH );
		//GL11.glEnable( GL11.GL_BLEND );
		//GL11.glBlendFunc( GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA );
		
		if( projection )
			setPerspective();
		else
			setOrtho();
		
		GL11.glMatrixMode( GL11.GL_MODELVIEW );
		GL11.glLoadIdentity();
	}
	
	/**
	 * 
	 */
	public void setPerspective() {
		GL11.glMatrixMode( GL11.GL_PROJECTION );
		GL11.glLoadIdentity();

		GLU.gluPerspective( 45.0f, (float)width/(float)height, 0.001f, 2000.0f );
	
		GL11.glMatrixMode( GL11.GL_MODELVIEW );
		GL11.glLoadIdentity();
	}
	
	/**
	 * 
	 */
	public void setOrtho() {
		GL11.glMatrixMode( GL11.GL_PROJECTION );
		GL11.glLoadIdentity();

		GL11.glOrtho( -width/2, width/2, height/2, -height/2, -1000, 1000 );
//		GL11.glOrtho( 0, width, height, 0, -1000, 10000 );
		
		GL11.glMatrixMode( GL11.GL_MODELVIEW );
		GL11.glLoadIdentity();
	}
	
	/**
	 * 
	 * @param delta
	 */
	public void renderGL( int delta ) {
		//GL11.glEnable( GL11.GL_DEPTH_TEST );
		//GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT | GL11.GL_STENCIL_BUFFER_BIT);
		
		root.rendering( delta );
	}

	/**
	 * 
	 * @return
	 */
	public ISea3 getRoot() {
		return root;
	}

	/**
	 * 
	 */
	@Override
	public void setUpdate() {
		makeUpdate = true;
	}

	/**
	 * 
	 */
	public void stopRunning() {
		running = false;
	}

	/**
	 * 
	 * @return
	 */
	public int getFPS() {
		return currFPS;
	}

	/**
	 * 
	 * @return
	 */
	public boolean isAsClient() {
		return asClient;
	}

	/**
	 * 
	 * @param asClient
	 */
	public void setAsClient( boolean asClient ) {
		this.asClient = asClient;
	}

	/**
	 * 
	 * @return
	 */
	public String getIp() {
		return ip;
	}

	/**
	 * 
	 * @param ip
	 */
	public void setIp( String ip ) {
		this.ip = ip;
	}

	/**
	 * 
	 * @return
	 */
	public long getUsedSpace() {
		return usedSpace;
	}

	public String getTitle() {
		return title;
	}

	public double getUnprocessedSeconds() {
		return unprocessedSeconds;
	}

	public long getLastTimeDebug() {
		return lastTimeDebug;
	}

	public void setIsRender( boolean isRender ) {
		this.isRender = isRender;
	}

	public void setIsUpdate( boolean isUpdate ) {
		this.isUpdate = isUpdate;
	}

	public float getFrameTimeAverage() {
		return frameTimeAverage;
	}

	public long getMaxSpace() {
		return maxSpace;
	}

	public float getUpdateTimeAverage() {
		return updateTimeAverage;
	}
	
}
